OBJETIVO: encontrar el número de puntos que cuenta una imagen

PASO 1: CARGAR Y MOSTRAR LA IMAGEN

In [104]:
import cv2

# Definimos una función para obtener la versión en escala de grises de la imagen
def imagen_redimensionada_gris(): 
    imagen_escala_grises = cv2.imread('Pintura_Puntos.jpg', 0) #el 0 em convierte la imagen a escala de grises
    nueva_altura = 600
    nueva_anchura = int((nueva_altura / imagen_escala_grises.shape[0]) * imagen_escala_grises.shape[1])
    imagen_redimensionada_gris = cv2.resize(imagen_escala_grises, (nueva_anchura, nueva_altura))
    return imagen_redimensionada_gris 

# Definimos una función que muestre la imagen redimensionada en escala de grises
def mostrar_imagen1():
    IMAGEN_GRIS = imagen_redimensionada_gris()
    cv2.imshow('Imagen', IMAGEN_GRIS)
    cv2.waitKey(0) #la imagen aparece indefinidamente hasta que se toque una tecla
    cv2.destroyAllWindows() #cuando se toque una tecla se cierra la ventana de visualización

# Llamamos a la función para mostrar la imagen 
mostrar_imagen1()

PASO 2: UMBRALIZACIÓN

Tenemos diferentes métodos para umbralizar, vamos a ir probando cada uno de ellos y elegir el que mejor se ajuste a mi imagen y el objetivo que buscamos. Contamos con los siguientes métodos: 1) UMBRALIZACIÓN GLOBAL 2) UMBRALIZACIÓN MÉTODO OTSU 3) UMBRALIZACIÓN ADAPTATIVA (Gauss y de media)

1) UMBRALIZACIÓN GLOBAL

Para realizar este método primero debemos asignarle nosotros un valor de umbral del que se va a basar para binarizar la imagen. Generalmente se le asigna el 127, porque es un valor que divide el histograma de la imagen en 2 (hay 256 valores posibles a los que puede optar mi píxel):

In [105]:
IMAGEN_GRIS = imagen_redimensionada_gris() #guardo en una variable mi imagen redimensionada en escala de grises

import matplotlib.pyplot as plt
def umbralización_global1(IMAGEN_GRIS):
    _, umbral_binario = cv2.threshold(IMAGEN_GRIS, 127, 255, cv2.THRESH_BINARY)
    plt.imshow(umbral_binario, cmap='gray')
    plt.axis('off') #quitamos los ejes
    plt.title('Umbralización global (127)')

umbralización_global1(IMAGEN_GRIS)

También podríamos haber asignado nosotros un valor de umbral observando el histograma de la imagen:

In [106]:
def histograma(IMAGEN_GRIS):
    # Calcular el histograma
    histograma = plt.hist(IMAGEN_GRIS.ravel(), bins=256, range=(0, 256))
    # Mostrar el histograma
    plt.title('Histograma de la imagen')
    plt.xlabel('Valor de píxel')
    plt.ylabel('Frecuencia')
    plt.show()

histograma(IMAGEN_GRIS)

Observamos el histograma y elegimos como umbral el 110

In [107]:
def umbralización_global2(IMAGEN_GRIS): 
    _, umbral_binario = cv2.threshold(IMAGEN_GRIS, 110, 255, cv2.THRESH_BINARY)
    plt.imshow(umbral_binario, cmap='gray')
    plt.axis('off')
    plt.title('Umbralización global (110)')

umbralización_global2(IMAGEN_GRIS)

El resultado mejora ya que se pueden observar más puntos, pero sigue sin ser el adecuado. Seguimos probando otros métodos:

2) UMBRALIZACIÓN OTSU

In [108]:
def umbralización_OTSU(IMAGEN_GRIS):
    _, umbral_binario = cv2.threshold(IMAGEN_GRIS, 110, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
    plt.imshow(umbral_binario, cmap='gray')
    plt.axis('off')
    plt.title('Umbralización OTSU') 

umbralización_OTSU(IMAGEN_GRIS)

El resultado es peor que con el de umbralización global

3) UMBRALIZACIÓN ADAPTATIVA

A) Umbralización adaptativa DE GAUSS

In [109]:
def umbralización_GAUSS(IMAGEN_GRIS):
    umbral_adaptativo_gauss = cv2.adaptiveThreshold(IMAGEN_GRIS, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2)
    plt.imshow(umbral_adaptativo_gauss, cmap='gray')
    plt.axis('off')
    plt.title('Umbralización adaptativa Gauss') 

umbralización_GAUSS(IMAGEN_GRIS)

B) Umbralización adaptativa media

In [110]:
def umbralización_media(IMAGEN_GRIS):
    umbral_adaptativo_media = cv2.adaptiveThreshold(IMAGEN_GRIS, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 11, 2)
    plt.imshow(umbral_adaptativo_media, cmap='gray')
    plt.axis('off')
    plt.title('Umbralización adaptativa media')
    
umbralización_media(IMAGEN_GRIS)

Mostramos los resultados de cada método en una gráfica para poder compararlos:

In [111]:
IMAGEN_GRIS = imagen_redimensionada_gris()
def umbralización_global(IMAGEN_GRIS):
    _, umbral_binario = cv2.threshold(IMAGEN_GRIS, 110, 255, cv2.THRESH_BINARY)
    plt.imshow(umbral_binario, cmap='gray')
    plt.axis('off')
    plt.title('Umbralización global (110)')

def umbralización_OTSU(IMAGEN_GRIS):
    _, umbral_binario = cv2.threshold(IMAGEN_GRIS, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
    plt.imshow(umbral_binario, cmap='gray')
    plt.axis('off')
    plt.title('Umbralización OTSU') 

def umbralización_GAUSS(IMAGEN_GRIS):
    umbral_adaptativo_gauss = cv2.adaptiveThreshold(IMAGEN_GRIS, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2)
    plt.imshow(umbral_adaptativo_gauss, cmap='gray')
    plt.axis('off')
    plt.title('Umbralización Gauss')

def umbralización_media(IMAGEN_GRIS):
    umbral_adaptativo_media = cv2.adaptiveThreshold(IMAGEN_GRIS, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 11, 2)
    plt.imshow(umbral_adaptativo_media, cmap='gray')
    plt.axis('off')
    plt.title('Umbralización media')


plt.figure(figsize=(12, 6))

plt.subplot(2, 2, 1)
umbralización_global(IMAGEN_GRIS)

plt.subplot(2, 2, 2)
umbralización_OTSU(IMAGEN_GRIS)

plt.subplot(2, 2, 3)
umbralización_GAUSS(IMAGEN_GRIS)

plt.subplot(2, 2, 4)
umbralización_media(IMAGEN_GRIS)

plt.tight_layout()
plt.show()
    

Observamos que la mejor umbralización se consigue a través del método adaptativo de Gauss. Con este se pueden observar cada uno de los puntos de la imagen, además que distingue bien el fondo de lo que son los puntos. Bien es cierto, que el resultado a través de umbralización media de aproxima al de Gauss, pero este último es ligeramente mejor puesto que consigue identificar los puntos de la línea central y no los pone tan en negro. El método global obtiene mejores resultados que el de otsu, pero aún así quedan descartados puesto que asigna a la mitad de los puntos de la imagen en negro y no se pueden ver.

Los métodos threshold_niblack y threshhold_sauvola corresponden a métodos de umbralización adaptativa.

In [112]:
import matplotlib.pyplot as plt
from skimage import io, color, filters
# Aplicar umbralización Niblack
umbral_niblack = filters.threshold_niblack(IMAGEN_GRIS, window_size=25, k=0.8)

# Aplicar umbralización Sauvola
umbral_sauvola = filters.threshold_sauvola(IMAGEN_GRIS, window_size=25)

# Binarizar la imagen con los umbrales calculados
imagen_umbral_niblack = IMAGEN_GRIS > umbral_niblack 
imagen_umbral_sauvola = IMAGEN_GRIS > umbral_sauvola
# píxel es mayor que el umbral, se establece como blanco sino como negro


# Mostrar las imágenes umbralizadas
plt.figure(figsize=(12, 6))

plt.subplot(1, 3, 1)
plt.imshow(IMAGEN_GRIS, cmap='gray')
plt.axis('off')
plt.title('Imagen en escala de grises')

plt.subplot(1, 3, 2)
plt.imshow(imagen_umbral_niblack, cmap='gray')
plt.axis('off')
plt.title('Umbralización Niblack')

plt.subplot(1, 3, 3)
plt.imshow(imagen_umbral_sauvola, cmap='gray')
plt.axis('off')
plt.title('Umbralización Sauvola')

plt.tight_layout()
plt.show()

Rotamos la imagen 180 grados y vemos si afecta en el resultado

In [113]:
imagen_rotada= cv2.rotate(IMAGEN_GRIS, cv2.ROTATE_180)
cv2.imshow('Imagen Rotada 180 grados', imagen_rotada)
cv2.waitKey(0)
cv2.destroyAllWindows()   
In [114]:
def umbralización_global(imagen_rotada):
    _, umbral_binario = cv2.threshold(imagen_rotada, 110, 255, cv2.THRESH_BINARY)
    plt.imshow(umbral_binario, cmap='gray')
    plt.axis('off')
    plt.title('Umbralización global (110)')

def umbralización_OTSU(imagen_rotada):
    _, umbral_binario = cv2.threshold(imagen_rotada, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
    plt.imshow(umbral_binario, cmap='gray')
    plt.axis('off')
    plt.title('Umbralización OTSU') 

def umbralización_GAUSS(imagen_rotada):
    umbral_adaptativo_gauss = cv2.adaptiveThreshold(imagen_rotada, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2)
    plt.imshow(umbral_adaptativo_gauss, cmap='gray')
    plt.axis('off')
    plt.title('Umbralización Gauss')

def umbralización_media(IMAGEN_GRIS):
    umbral_adaptativo_media = cv2.adaptiveThreshold(imagen_rotada, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 11, 2)
    plt.imshow(umbral_adaptativo_media, cmap='gray')
    plt.axis('off')
    plt.title('Umbralización media')


plt.figure(figsize=(12, 6))

plt.subplot(2, 2, 1)
umbralización_global(imagen_rotada)

plt.subplot(2, 2, 2)
umbralización_OTSU(imagen_rotada)

plt.subplot(2, 2, 3)
umbralización_GAUSS(imagen_rotada)

plt.subplot(2, 2, 4)
umbralización_media(imagen_rotada)

plt.tight_layout()
plt.show()
    

Observamos que no influye el que giremos la imagen 180 grados. La rotación de la imagen no debería influir en el proceso de umbralización, ya que este proceso se realiza en función de los valores de píxeles y no de la orientación de la imagen.

PASO 3: MORFOLOGÍA MATEMÁTICA

In [115]:
#Guardamos la imagen umbralizada en una variable
def umbralización_GAUSS(IMAGEN_GRIS):
    umbral_adaptativo_gauss = cv2.adaptiveThreshold(IMAGEN_GRIS, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2)
    return umbral_adaptativo_gauss  # Retorna la imagen umbralizada

# Llamada a la función para obtener la imagen umbralizada
imagen_umbralizada = umbralización_GAUSS(IMAGEN_GRIS)

Intentamos elegir como elemento estructural un círculo, ya que es la que mejor se adecúa a nuestro caso porque la imagen está hecha de puntos

In [116]:
# Define el radio del círculo
radio = 1 
# Crea un elemento estructural circular
elemento_estructural = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (2*radio+1, 2*radio+1))

# El tamaño del elemento estructural será (2*radio+1) x (2*radio+1)

# Muestra el elemento estructural
print("Elemento Estructural Circular:")
print(elemento_estructural)
Elemento Estructural Circular:
[[0 1 0]
 [1 1 1]
 [0 1 0]]

al ponerle radio 1 el elemento estructural pasa a ser una cruz. He probado diferentes radios más grandes de 1 pero eran demasiado y se come toda la imagen cuando aplicamos operaciones de erosión o dilatación

Primero vamos a probar como se ve la imagen erosionando y dilatando por separado

In [117]:
#Aplicar la erosión a la imagen umbralizada
imagen_erosionada1 = cv2.erode(imagen_umbralizada, elemento_estructural, iterations=1)

# Aplicar la dilatación a la imagen umbralizada
imagen_dilatada = cv2.dilate(imagen_umbralizada, elemento_estructural, iterations=1)

# Crear una figura con dos subplots para mostrar ambas imágenes lado a lado
plt.figure(figsize=(12, 6))

# Subplot 1: Imagen Erosionada
plt.subplot(1, 2, 1)
plt.imshow(imagen_erosionada1, cmap='gray')
plt.axis('off')  # Oculta los ejes
plt.title('Imagen Erosionada')

# Subplot 2: Imagen Dilatada
plt.subplot(1, 2, 2)
plt.imshow(imagen_dilatada, cmap='gray')
plt.axis('off')  # Oculta los ejes
plt.title('Imagen Dilatada')

# Mostrar la figura que contiene ambos subplots
plt.tight_layout()  # Ajusta automáticamente el espacio entre subplots
plt.show()

Viendo la imagen es mejor empezar erosionando la imagen. Así que erosionamos y aplicamos una dilatación, es decir, aplicamos una apertura.

In [118]:
# Aplicar la apertura (erosión seguida de dilatación)
imagen_apertura = cv2.morphologyEx(imagen_umbralizada, cv2.MORPH_OPEN, elemento_estructural, iterations=1)
plt.imshow(imagen_apertura, cmap='gray')
plt.axis('off')  # Oculta los ejes
plt.title('Imagen después de la Apertura (Opening)')
Out[118]:
Text(0.5, 1.0, 'Imagen después de la Apertura (Opening)')

Se come bastantes puntos pero es la mejor aproximación que he obtenido de las que he ido probando

PASO 4: CONTANDO CÍRCULOS

In [121]:
import cv2
import numpy as np

def contar_circulos(imagen_apertura):
    # Comprobar si la imagen es binaria
    if len(imagen_apertura.shape) != 2 or imagen_apertura.dtype != np.uint8:
        print("La imagen no es binaria.")
        return None
    
    # Encontrar los contornos en la imagen
    contornos, _ = cv2.findContours(imagen_apertura, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    
    # Contar el número de círculos encontrados
    numero_de_circulos = 0
    for contorno in contornos:
        # Calcular el área del contorno para determinar si es un círculo
        area = cv2.contourArea(contorno)
        if area > 11:  # Se ajusta este umbral según necesidades
            numero_de_circulos += 1
    
    return numero_de_circulos
In [122]:
contar_circulos(imagen_apertura)
Out[122]:
4411

Obtenemos que la imagen tiene aproximadamente 4000 puntos

Para mejorar la precisión en la detección de círculos después de la umbralización por posible ruido que haya llegado a este punto se podría:

Ajustar el umbral de área: El umbral de área utilizado para identificar círculos debe ajustarse según las características de la imagen. Esto es especialmente importante si los puntos varían significativamente en tamaño en comparación con otros objetos en la imagen.

Filtrar por forma: Además de evaluar el área, puedes considerar la forma de los contornos. Calcula índices de circularidad o relaciones entre perímetro y área para identificar contornos que se asemejen a círculos.

Eliminar contornos pequeños: Antes de contar los círculos, aplica un filtro para eliminar contornos muy pequeños que puedan representar ruido o detalles insignificantes.

Explora métodos de umbralización alternativos: Experimenta con diferentes métodos de umbralización, como Adaptive Thresholding o técnicas basadas en detección de bordes, para mejorar la precisión en la umbralización inicial.

Aplica preprocesamiento: Antes de la umbralización, considera realizar operaciones de preprocesamiento, como suavizado o reducción de ruido, para mejorar la calidad de la imagen y reducir los errores en la umbralización.

La elección de la estrategia adecuada dependerá de las características específicas de tus imágenes y de los tipos de errores que deseas abordar.

PASO 5: AUTOMATIZAR PROCESO DE EXTRACCIÓN

El siguientecódigo explorará todas las combinaciones posibles de parámetros dentro de los intervalos definidos y registrará la mejor combinación y el número máximo de puntos encontrados

In [123]:
# Cargar la imagen en color
imagen_color = cv2.imread('Pintura_Puntos.jpg')

# Convertir la imagen en color a escala de grises
imagen_gris = cv2.cvtColor(imagen_color, cv2.COLOR_BGR2GRAY)

# Define los intervalos de parámetros a explorar
umbral_values = [5, 10, 15]
radio_values = [1, 2, 3]

# Inicializa variables para mantener el mejor resultado y la mejor combinación de parámetros
mejor_resultado = 0
mejor_combinacion = None

# Itera a través de todas las combinaciones de parámetros
for umbral, radio_estructural in itertools.product(umbral_values, radio_values):
    # Procesa la imagen con los parámetros actuales y cuenta los puntos
    num_puntos = procesar_y_contar_puntos(imagen_gris, umbral, radio_estructural)

    # Si encontramos una mejor combinación, actualiza el resultado y la combinación
    if num_puntos > mejor_resultado:
        mejor_resultado = num_puntos
        mejor_combinacion = (umbral, radio_estructural)

# Muestra la combinación ganadora y el número de puntos
print(f'La mejor combinación de parámetros es: Umbral = {mejor_combinacion[0]}, Radio Estructural = {mejor_combinacion[1]}')
print(f'Número máximo de puntos encontrados: {mejor_resultado}')
La mejor combinación de parámetros es: Umbral = 5, Radio Estructural = 3
Número máximo de puntos encontrados: 7015